package test;

import javax.swing.*;
import javax.imageio.*;
import javax.media.opengl.*;
import javax.media.opengl.glu.*;

import java.awt.*;
import java.awt.event.*;
import java.nio.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.util.*;

import java.util.List;

import com.sun.opengl.util.*;

public class Jogl {
	public static void main(String[] args) {
		GLDisplay neheGLDisplay = GLDisplay.createGLDisplay("Lesson 08: Blending");
        Renderer renderer = new Renderer();
        InputHandler inputHandler = new InputHandler(renderer, neheGLDisplay);
        neheGLDisplay.addGLEventListener(renderer);
        neheGLDisplay.addKeyListener(inputHandler);
        neheGLDisplay.start();
	}
}

class Renderer implements GLEventListener {
    private boolean lightingEnabled;				// Lighting ON/OFF
    private boolean lightingChanged = false;		// Lighting changed
    private boolean blendingEnabled;				// Blending OFF/ON
    private boolean blendingChanged = false;		// Blending changed

    private int filter;				                // Which texture to use
    private int[] textures = new int[3];			// Storage For 3 Textures

    private float xrot;				// X Rotation
    private float yrot;				// Y Rotation

    private float xspeed = 0.5f;				// X Rotation Speed
    private boolean increaseX;
    private boolean decreaseX;

    private float yspeed = 0.3f;				// Y Rotation Speed
    private boolean increaseY;
    private boolean decreaseY;

    private float z = -5.0f;			// Depth Into The Screen
    private boolean zoomIn;
    private boolean zoomOut;

    private float[] lightAmbient = {0.5f, 0.5f, 0.5f, 1.0f};
    private float[] lightDiffuse = {1.0f, 1.0f, 1.0f, 1.0f};
    private float[] lightPosition = {0.0f, 0.0f, 2.0f, 1.0f};

    private GLU glu = new GLU();

    public void toggleBlending() {
        blendingEnabled = !blendingEnabled;
        blendingChanged = true;
    }

    public void toggleLighting() {
        lightingEnabled = !lightingEnabled;
        lightingChanged = true;
    }

    public void increaseXspeed(boolean increase) {
        increaseX = increase;
    }

    public void decreaseXspeed(boolean decrease) {
        decreaseX = decrease;
    }

    public void increaseYspeed(boolean increase) {
        increaseY = increase;
    }

    public void decreaseYspeed(boolean decrease) {
        decreaseY = decrease;
    }

    public void zoomIn(boolean zoom) {
        zoomIn = zoom;
    }

    public void zoomOut(boolean zoom) {
        zoomOut = zoom;
    }

    public void switchFilter() {
        filter = (filter + 1) % textures.length;
    }

    private void loadGLTextures(GLAutoDrawable gldrawable) throws IOException {
        TextureReader.Texture texture = TextureReader.readTexture("glass.png");

        GL gl = gldrawable.getGL();

        //Create Nearest Filtered Texture
        gl.glGenTextures(3, textures, 0);
        gl.glBindTexture(GL.GL_TEXTURE_2D, textures[0]);

        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);

        gl.glTexImage2D(GL.GL_TEXTURE_2D,
                0,
                3,
                texture.getWidth(),
                texture.getHeight(),
                0,
                GL.GL_RGB,
                GL.GL_UNSIGNED_BYTE,
                texture.getPixels());

        //Create Linear Filtered Texture
        gl.glBindTexture(GL.GL_TEXTURE_2D, textures[1]);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);

        gl.glTexImage2D(GL.GL_TEXTURE_2D,
                0,
                3,
                texture.getWidth(),
                texture.getHeight(),
                0,
                GL.GL_RGB,
                GL.GL_UNSIGNED_BYTE,
                texture.getPixels());

        //Create MipMapped Texture (Only with GL4Java 2.1.2.1 and later!)
        gl.glBindTexture(GL.GL_TEXTURE_2D, textures[2]);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
        gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR_MIPMAP_NEAREST);

        glu.gluBuild2DMipmaps(GL.GL_TEXTURE_2D,
                3,
                texture.getWidth(),
                texture.getHeight(),
                GL.GL_RGB,
                GL.GL_UNSIGNED_BYTE,
                texture.getPixels());
    }

    public void init(GLAutoDrawable glDrawable) {
        try {
            loadGLTextures(glDrawable);
        } catch (IOException e) {
            System.out.println("Unable to load textures,Bailing!");
            throw new RuntimeException(e);
        }

        GL gl = glDrawable.getGL();
        gl.glEnable(GL.GL_TEXTURE_2D);							// Enable Texture Mapping
        gl.glShadeModel(GL.GL_SMOOTH);                            //Enables Smooth Color Shading
        gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //This Will Clear The Background Color To Black
        gl.glClearDepth(1.0);                                  //Enables Clearing Of The Depth Buffer
        gl.glEnable(GL.GL_DEPTH_TEST);                            //Enables Depth Testing
        gl.glDepthFunc(GL.GL_LEQUAL);                             //The Type Of Depth Test To Do
        gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);  // Really Nice Perspective Calculations
        gl.glLightfv(GL.GL_LIGHT1, GL.GL_AMBIENT, lightAmbient, 0);		// Setup The Ambient Light
        gl.glLightfv(GL.GL_LIGHT1, GL.GL_DIFFUSE, lightDiffuse, 0);		// Setup The Diffuse Light
        gl.glLightfv(GL.GL_LIGHT1, GL.GL_POSITION, lightPosition, 0);	// Position The Light
        gl.glEnable(GL.GL_LIGHT1);								// Enable Light One

        gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f);					// Full Brightness.  50% Alpha (new )
        gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE);					// Set The Blending Function For Translucency (new )
    }

    private void update() {
        if (zoomOut)
            z -= 0.02f;
        if (zoomIn)
            z += 0.02f;
        if (decreaseX)
            xspeed -= 0.01f;
        if (increaseX)
            xspeed += 0.01f;
        if (increaseY)
            yspeed += 0.01f;
        if (decreaseY)
            yspeed -= 0.01f;
    }

    public void display(GLAutoDrawable glDrawable) {
        update();
        GL gl = glDrawable.getGL();
        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);       //Clear The Screen And The Depth Buffer
        gl.glLoadIdentity();                                         //Reset The View
        gl.glTranslatef(0.0f, 0.0f, z);

        gl.glRotatef(xrot, 1.0f, 0.0f, 0.0f);
        gl.glRotatef(yrot, 0.0f, 1.0f, 0.0f);

        gl.glBindTexture(GL.GL_TEXTURE_2D, textures[filter]);

        gl.glBegin(GL.GL_QUADS);
        // Front Face
        gl.glNormal3f(0.0f, 0.0f, 1.0f);
        gl.glTexCoord2f(0.0f, 0.0f);
        gl.glVertex3f(-1.0f, -1.0f, 1.0f);
        gl.glTexCoord2f(1.0f, 0.0f);
        gl.glVertex3f(1.0f, -1.0f, 1.0f);
        gl.glTexCoord2f(1.0f, 1.0f);
        gl.glVertex3f(1.0f, 1.0f, 1.0f);
        gl.glTexCoord2f(0.0f, 1.0f);
        gl.glVertex3f(-1.0f, 1.0f, 1.0f);
        // Back Face
        gl.glNormal3f(0.0f, 0.0f, -1.0f);
        gl.glTexCoord2f(1.0f, 0.0f);
        gl.glVertex3f(-1.0f, -1.0f, -1.0f);
        gl.glTexCoord2f(1.0f, 1.0f);
        gl.glVertex3f(-1.0f, 1.0f, -1.0f);
        gl.glTexCoord2f(0.0f, 1.0f);
        gl.glVertex3f(1.0f, 1.0f, -1.0f);
        gl.glTexCoord2f(0.0f, 0.0f);
        gl.glVertex3f(1.0f, -1.0f, -1.0f);
        // Top Face
        gl.glNormal3f(0.0f, 1.0f, 0.0f);
        gl.glTexCoord2f(0.0f, 1.0f);
        gl.glVertex3f(-1.0f, 1.0f, -1.0f);
        gl.glTexCoord2f(0.0f, 0.0f);
        gl.glVertex3f(-1.0f, 1.0f, 1.0f);
        gl.glTexCoord2f(1.0f, 0.0f);
        gl.glVertex3f(1.0f, 1.0f, 1.0f);
        gl.glTexCoord2f(1.0f, 1.0f);
        gl.glVertex3f(1.0f, 1.0f, -1.0f);
        // Bottom Face
        gl.glNormal3f(0.0f, -1.0f, 0.0f);
        gl.glTexCoord2f(1.0f, 1.0f);
        gl.glVertex3f(-1.0f, -1.0f, -1.0f);
        gl.glTexCoord2f(0.0f, 1.0f);
        gl.glVertex3f(1.0f, -1.0f, -1.0f);
        gl.glTexCoord2f(0.0f, 0.0f);
        gl.glVertex3f(1.0f, -1.0f, 1.0f);
        gl.glTexCoord2f(1.0f, 0.0f);
        gl.glVertex3f(-1.0f, -1.0f, 1.0f);
        // Right face
        gl.glNormal3f(1.0f, 0.0f, 0.0f);
        gl.glTexCoord2f(1.0f, 0.0f);
        gl.glVertex3f(1.0f, -1.0f, -1.0f);
        gl.glTexCoord2f(1.0f, 1.0f);
        gl.glVertex3f(1.0f, 1.0f, -1.0f);
        gl.glTexCoord2f(0.0f, 1.0f);
        gl.glVertex3f(1.0f, 1.0f, 1.0f);
        gl.glTexCoord2f(0.0f, 0.0f);
        gl.glVertex3f(1.0f, -1.0f, 1.0f);
        // Left Face
        gl.glNormal3f(-1.0f, 0.0f, 0.0f);
        gl.glTexCoord2f(0.0f, 0.0f);
        gl.glVertex3f(-1.0f, -1.0f, -1.0f);
        gl.glTexCoord2f(1.0f, 0.0f);
        gl.glVertex3f(-1.0f, -1.0f, 1.0f);
        gl.glTexCoord2f(1.0f, 1.0f);
        gl.glVertex3f(-1.0f, 1.0f, 1.0f);
        gl.glTexCoord2f(0.0f, 1.0f);
        gl.glVertex3f(-1.0f, 1.0f, -1.0f);
        gl.glEnd();

        xrot += xspeed;
        yrot += yspeed;

        // toggle lighting
        if (lightingChanged) {
            if (lightingEnabled)
                gl.glEnable(GL.GL_LIGHTING);
            else
                gl.glDisable(GL.GL_LIGHTING);
            lightingChanged = false;
        }

        // toggle blending
        if (blendingChanged) {
            if (blendingEnabled) {
                gl.glEnable(GL.GL_BLEND);		// Turn Blending On
                gl.glDisable(GL.GL_DEPTH_TEST);	// Turn Depth Testing Off
            } else {
                gl.glDisable(GL.GL_BLEND);		// Turn Blending Off
                gl.glEnable(GL.GL_DEPTH_TEST);	// Turn Depth Testing On
            }
            blendingChanged = false;
        }
    }

    public void reshape(GLAutoDrawable glDrawable, int x, int y, int w, int h) {
        final GL gl = glDrawable.getGL();

        if (h <= 0) // avoid a divide by zero error!
            h = 1;
        final float a = (float) w / (float) h;
        gl.glViewport(0, 0, w, h);
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glLoadIdentity();
        glu.gluPerspective(45.0f, a, 1.0, 20.0);
        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glLoadIdentity();                                     // Reset The ModalView Matrix
    }

    public void displayChanged(GLAutoDrawable glDrawable, boolean b, boolean b1) {
    }
}



class InputHandler extends KeyAdapter {
    private Renderer renderer;

    public InputHandler(Renderer renderer, GLDisplay glDisplay) {
        this.renderer = renderer;
        glDisplay.registerKeyStrokeForHelp(KeyStroke.getKeyStroke(KeyEvent.VK_L, 0), "Toggle lighting");
        glDisplay.registerKeyStrokeForHelp(KeyStroke.getKeyStroke(KeyEvent.VK_F, 0), "Switch texture filter");
        glDisplay.registerKeyStrokeForHelp(KeyStroke.getKeyStroke(KeyEvent.VK_B, 0), "Toggle blending");
        glDisplay.registerKeyStrokeForHelp(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "Zoom in");
        glDisplay.registerKeyStrokeForHelp(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "Zoom out");
        glDisplay.registerKeyStrokeForHelp(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "Rotate slower along X-axis");
        glDisplay.registerKeyStrokeForHelp(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "Rotate faster along X-axis");
        glDisplay.registerKeyStrokeForHelp(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "Rotate slower along Y-axis");
        glDisplay.registerKeyStrokeForHelp(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "Rotate faster along Y-axis");
    }

    public void keyPressed(KeyEvent e) {
        processKeyEvent(e, true);
    }

    public void keyReleased(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_L:
                renderer.toggleLighting();
                break;
            case KeyEvent.VK_F:
                renderer.switchFilter();
                break;
            case KeyEvent.VK_B:
                renderer.toggleBlending();
                break;
            default:
                processKeyEvent(e, false);
        }
    }

    private void processKeyEvent(KeyEvent e, boolean pressed) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_PAGE_UP:
                renderer.zoomIn(pressed);
                break;
            case KeyEvent.VK_PAGE_DOWN:
                renderer.zoomOut(pressed);
                break;
            case KeyEvent.VK_UP:
                renderer.increaseXspeed(pressed);
                break;
            case KeyEvent.VK_DOWN:
                renderer.decreaseXspeed(pressed);
                break;
            case KeyEvent.VK_RIGHT:
                renderer.increaseYspeed(pressed);
                break;
            case KeyEvent.VK_LEFT:
                renderer.decreaseYspeed(pressed);
                break;
        }
    }
}


class GLDisplay {
    private static final int DEFAULT_WIDTH = 640;
    private static final int DEFAULT_HEIGHT = 480;

    private static final int DONT_CARE = -1;

    private JFrame frame;
    private GLCanvas glCanvas;
    //private FPSAnimator animator;
    private boolean fullscreen;
    private int width;
    private int height;
    private GraphicsDevice usedDevice;

    private MyHelpOverlayGLEventListener helpOverlayGLEventListener = new MyHelpOverlayGLEventListener();
    private MyExceptionHandler exceptionHandler = new MyExceptionHandler();

    public static GLDisplay createGLDisplay( String title ) {
        return createGLDisplay( title, new GLCapabilities() );
    }

    public static GLDisplay createGLDisplay( String title, GLCapabilities caps ) {
       // GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
        boolean fullscreen = false;
//        if ( device.isFullScreenSupported() ) {
//            int selectedOption = JOptionPane.showOptionDialog(
//                    null,
//                    "How would you like to run this demo?",
//                    title,
//                    JOptionPane.YES_NO_OPTION,
//                    JOptionPane.QUESTION_MESSAGE,
//                    null,
//                    new Object[]{"Fullscreen", "Windowed"},
//                    "Windowed" );
//            fullscreen = selectedOption == 0;
//        }
        return new GLDisplay( title, DEFAULT_WIDTH, DEFAULT_HEIGHT, fullscreen, caps );
    }

    private GLDisplay(String title, int width, int height, boolean fullscreen, GLCapabilities caps) {
        glCanvas = new GLCanvas(caps);
        glCanvas.setSize( width, height );
        glCanvas.setIgnoreRepaint( false );
        glCanvas.addGLEventListener( helpOverlayGLEventListener );

        frame = new JFrame( title );
        frame.getContentPane().setLayout( new BorderLayout() );
        frame.getContentPane().add( glCanvas, BorderLayout.CENTER );

        addKeyListener( new MyKeyAdapter() );

        this.fullscreen = fullscreen;
        this.width = width;
        this.height = height;

        //animator = new FPSAnimator( glCanvas, 60 );
        //animator.setRunAsFastAsPossible(false);
    }

    public void start() {
        try {
            Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
            frame.setUndecorated( fullscreen );

            frame.addWindowListener( new MyWindowAdapter() );

            if ( fullscreen ) {
                usedDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
                usedDevice.setFullScreenWindow( frame );
                usedDevice.setDisplayMode(
                        findDisplayMode(
                                usedDevice.getDisplayModes(),
                                width, height,
                                usedDevice.getDisplayMode().getBitDepth(),
                                usedDevice.getDisplayMode().getRefreshRate()
                        )
                );
            } else {
                frame.setSize( frame.getContentPane().getPreferredSize() );
                frame.setLocation(
                        ( screenSize.width - frame.getWidth() ) / 2,
                        ( screenSize.height - frame.getHeight() ) / 2
                );
                frame.setVisible( true );
            }

            glCanvas.requestFocus();

            //animator.start();
        } catch ( Exception e ) {
            exceptionHandler.handleException( e );
        }
    }

    public void stop() {
        try {
            //animator.stop();
            if ( fullscreen ) {
                usedDevice.setFullScreenWindow( null );
                usedDevice = null;
            }
            frame.dispose();
        } catch ( Exception e ) {
            exceptionHandler.handleException( e );
        } finally {
            System.exit( 0 );
        }
    }

    private DisplayMode findDisplayMode( DisplayMode[] displayModes, int requestedWidth, int requestedHeight, int requestedDepth, int requestedRefreshRate ) {
        // Try to find an exact match
        DisplayMode displayMode = findDisplayModeInternal( displayModes, requestedWidth, requestedHeight, requestedDepth, requestedRefreshRate );

        // Try again, ignoring the requested bit depth
        if ( displayMode == null )
            displayMode = findDisplayModeInternal( displayModes, requestedWidth, requestedHeight, DONT_CARE, DONT_CARE );

        // Try again, and again ignoring the requested bit depth and height
        if ( displayMode == null )
            displayMode = findDisplayModeInternal( displayModes, requestedWidth, DONT_CARE, DONT_CARE, DONT_CARE );

        // If all else fails try to get any display mode
        if ( displayMode == null )
            displayMode = findDisplayModeInternal( displayModes, DONT_CARE, DONT_CARE, DONT_CARE, DONT_CARE );

        return displayMode;
    }

    private DisplayMode findDisplayModeInternal( DisplayMode[] displayModes, int requestedWidth, int requestedHeight, int requestedDepth, int requestedRefreshRate ) {
        DisplayMode displayModeToUse = null;
        for ( int i = 0; i < displayModes.length; i++ ) {
            DisplayMode displayMode = displayModes[i];
            if ( ( requestedWidth == DONT_CARE || displayMode.getWidth() == requestedWidth ) &&
                    ( requestedHeight == DONT_CARE || displayMode.getHeight() == requestedHeight ) &&
                    ( requestedHeight == DONT_CARE || displayMode.getRefreshRate() == requestedRefreshRate ) &&
                    ( requestedDepth == DONT_CARE || displayMode.getBitDepth() == requestedDepth ) )
                displayModeToUse = displayMode;
        }

        return displayModeToUse;
    }

    public void addGLEventListener( GLEventListener glEventListener ) {
        this.helpOverlayGLEventListener.addGLEventListener( glEventListener );
    }

    public void removeGLEventListener( GLEventListener glEventListener ) {
        this.helpOverlayGLEventListener.removeGLEventListener( glEventListener );
    }

    public void addKeyListener( KeyListener l ) {
        glCanvas.addKeyListener( l );
    }

    public void addMouseListener( MouseListener l ) {
        glCanvas.addMouseListener( l );
    }

    public void addMouseMotionListener( MouseMotionListener l ) {
        glCanvas.addMouseMotionListener( l );
    }

    public void removeKeyListener( KeyListener l ) {
        glCanvas.removeKeyListener( l );
    }

    public void removeMouseListener( MouseListener l ) {
        glCanvas.removeMouseListener( l );
    }

    public void removeMouseMotionListener( MouseMotionListener l ) {
        glCanvas.removeMouseMotionListener( l );
    }

    public void registerKeyStrokeForHelp( KeyStroke keyStroke, String description ) {
        helpOverlayGLEventListener.registerKeyStroke( keyStroke, description );
    }

    public void registerMouseEventForHelp( int id, int modifiers, String description ) {
        helpOverlayGLEventListener.registerMouseEvent( id, modifiers, description );
    }

    public String getTitle() {
        return frame.getTitle();
    }

    public void setTitle( String title ) {
        frame.setTitle( title );
    }


    private class MyKeyAdapter extends KeyAdapter {
        public MyKeyAdapter() {
            registerKeyStrokeForHelp( KeyStroke.getKeyStroke( KeyEvent.VK_F1, 0 ), "Show/hide help message" );
            registerKeyStrokeForHelp( KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ), "Quit demo" );
        }

        public void keyReleased( KeyEvent e ) {
            switch ( e.getKeyCode() ) {
                case KeyEvent.VK_ESCAPE:
                    stop();
                    break;
                case KeyEvent.VK_F1:
                    helpOverlayGLEventListener.toggleHelp();
                    break;
            }
        }
    }

    private class MyWindowAdapter extends WindowAdapter {
        public void windowClosing( WindowEvent e ) {
            stop();
        }
    }

    private class MyExceptionHandler implements ExceptionHandler {
        public void handleException( final Exception e ) {
            SwingUtilities.invokeLater( new Runnable() {
                public void run() {
                    StringWriter stringWriter = new StringWriter();
                    PrintWriter printWriter = new PrintWriter( stringWriter );
                    e.printStackTrace( printWriter );
                    JOptionPane.showMessageDialog( frame, stringWriter.toString(), "Exception occurred", JOptionPane.ERROR_MESSAGE );
                    stop();
                }
            } );
        }
    }

    private static class MyHelpOverlayGLEventListener implements GLEventListener {
        private java.util.List<GLEventListener> eventListeners = new ArrayList<GLEventListener>();
        private HelpOverlay helpOverlay = new HelpOverlay();
        private boolean showHelp = false;

        public void toggleHelp() {
            showHelp = !showHelp;
        }

        public void registerKeyStroke( KeyStroke keyStroke, String description ) {
            helpOverlay.registerKeyStroke( keyStroke, description );
        }

        public void registerMouseEvent( int id, int modifiers, String description ) {
            helpOverlay.registerMouseEvent( id, modifiers, description );
        }

        public void addGLEventListener( GLEventListener glEventListener ) {
            eventListeners.add( glEventListener );
        }

        public void removeGLEventListener( GLEventListener glEventListener ) {
            eventListeners.remove( glEventListener );
        }

        public void display( GLAutoDrawable glDrawable ) {
            for ( int i = 0; i < eventListeners.size(); i++ ) {
                eventListeners.get( i ).display( glDrawable );
            }
            if ( showHelp )
                helpOverlay.display( glDrawable );
        }

        public void displayChanged( GLAutoDrawable glDrawable, boolean b, boolean b1 ) {
            for ( int i = 0; i < eventListeners.size(); i++ ) {
                eventListeners.get( i ).displayChanged( glDrawable, b, b1 );
            }
        }

        public void init( GLAutoDrawable glDrawable ) {
            for ( int i = 0; i < eventListeners.size(); i++ ) {
                eventListeners.get( i ).init( glDrawable );
            }
        }

        public void reshape( GLAutoDrawable glDrawable, int i0, int i1, int i2, int i3 ) {
            for ( int i = 0; i < eventListeners.size(); i++ ) {
                eventListeners.get( i ).reshape( glDrawable, i0, i1, i2, i3 );
            }
        }
    }
}

class TextureReader {
    public static Texture readTexture(String filename) throws IOException {
        return readTexture(filename, false);
    }

    public static Texture readTexture(String filename, boolean storeAlphaChannel) throws IOException {
        BufferedImage bufferedImage;
        if (filename.endsWith(".bmp")) {
            bufferedImage = BitmapLoader.loadBitmap(filename);
        } else {
            bufferedImage = readImage(filename);
        }
        return readPixels(bufferedImage, storeAlphaChannel);
    }

    private static BufferedImage readImage(String resourceName) throws IOException {
        return ImageIO.read(ResourceRetriever.getResourceAsStream(resourceName));
    }

    private static Texture readPixels(BufferedImage img, boolean storeAlphaChannel) {
        int[] packedPixels = new int[img.getWidth() * img.getHeight()];

        PixelGrabber pixelgrabber = new PixelGrabber(img, 0, 0, img.getWidth(), img.getHeight(), packedPixels, 0, img.getWidth());
        try {
            pixelgrabber.grabPixels();
        } catch (InterruptedException e) {
            throw new RuntimeException();
        }

        int bytesPerPixel = storeAlphaChannel ? 4 : 3;
        ByteBuffer unpackedPixels = BufferUtil.newByteBuffer(packedPixels.length * bytesPerPixel);

        for (int row = img.getHeight() - 1; row >= 0; row--) {
            for (int col = 0; col < img.getWidth(); col++) {
                int packedPixel = packedPixels[row * img.getWidth() + col];
                unpackedPixels.put((byte) ((packedPixel >> 16) & 0xFF));
                unpackedPixels.put((byte) ((packedPixel >> 8) & 0xFF));
                unpackedPixels.put((byte) ((packedPixel >> 0) & 0xFF));
                if (storeAlphaChannel) {
                    unpackedPixels.put((byte) ((packedPixel >> 24) & 0xFF));
                }
            }
        }

        unpackedPixels.flip();


        return new Texture(unpackedPixels, img.getWidth(), img.getHeight());
    }

    public static class Texture {
        private ByteBuffer pixels;
        private int width;
        private int height;

        public Texture(ByteBuffer pixels, int width, int height) {
            this.height = height;
            this.pixels = pixels;
            this.width = width;
        }

        public int getHeight() {
            return height;
        }

        public ByteBuffer getPixels() {
            return pixels;
        }

        public int getWidth() {
            return width;
        }
    }
}

interface ExceptionHandler {
    void handleException(Exception e);
}

class HelpOverlay implements GLEventListener {
    private List<String> keyboardEntries = new ArrayList<String>();
    private List<String> mouseEntries = new ArrayList<String>();
    private boolean visible = false;
    private GLUT glut = new GLUT();
    private GLU glu = new GLU();
    private static final int CHAR_HEIGHT = 12;
    private static final int OFFSET = 15;
    private static final int INDENT = 3;
    private static final String KEYBOARD_CONTROLS = "Keyboard controls";
    private static final String MOUSE_CONTROLS = "Mouse controls";

    public boolean isVisible() {
        return visible;
    }

    public void setVisible(boolean visible) {
        this.visible = visible;
    }

    public void display(GLAutoDrawable glDrawable) {
        GL gl = glDrawable.getGL();

        // Store old matrices
        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPushMatrix();
        gl.glLoadIdentity();
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glPushMatrix();
        gl.glLoadIdentity();

        gl.glViewport(0, 0, glDrawable.getWidth(), glDrawable.getHeight());

        // Store enabled state and disable lighting, texture mapping and the depth buffer
        gl.glPushAttrib(GL.GL_ENABLE_BIT);
        gl.glDisable(GL.GL_BLEND);
        gl.glDisable(GL.GL_LIGHTING);
        gl.glDisable(GL.GL_TEXTURE_2D);
        gl.glDisable(GL.GL_DEPTH_TEST);

        // Retrieve the current viewport and switch to orthographic mode
        IntBuffer viewPort = BufferUtil.newIntBuffer(4);
        gl.glGetIntegerv(GL.GL_VIEWPORT, viewPort);
        glu.gluOrtho2D(0, viewPort.get(2), viewPort.get(3), 0);

        // Render the text
        gl.glColor3f(1, 1, 1);

        int x = OFFSET;
        int maxx = 0;
        int y = OFFSET + CHAR_HEIGHT;

        if (keyboardEntries.size() > 0) {
            gl.glRasterPos2i(x, y);
            glut.glutBitmapString(GLUT.BITMAP_HELVETICA_12, KEYBOARD_CONTROLS);
            maxx = Math.max(maxx, OFFSET + glut.glutBitmapLength(GLUT.BITMAP_HELVETICA_12, KEYBOARD_CONTROLS));

            y += OFFSET;
            x += INDENT;
            for (int i = 0; i < keyboardEntries.size(); i++) {
                gl.glRasterPos2f(x, y);
                String text = (String) keyboardEntries.get(i);
                glut.glutBitmapString(GLUT.BITMAP_HELVETICA_12, text);
                maxx = Math.max(maxx, OFFSET + glut.glutBitmapLength(GLUT.BITMAP_HELVETICA_12, text));
                y += OFFSET;
            }
        }

        if (mouseEntries.size() > 0) {
            x = maxx + OFFSET;
            y = OFFSET + CHAR_HEIGHT;
            gl.glRasterPos2i(x, y);
            glut.glutBitmapString(GLUT.BITMAP_HELVETICA_12, MOUSE_CONTROLS);

            y += OFFSET;
            x += INDENT;
            for (int i = 0; i < mouseEntries.size(); i++) {
                gl.glRasterPos2f(x, y);
                glut.glutBitmapString(GLUT.BITMAP_HELVETICA_12, (String) mouseEntries.get(i));
                y += OFFSET;
            }
        }

        // Restore enabled state
        gl.glPopAttrib();

        // Restore old matrices
        gl.glPopMatrix();
        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPopMatrix();
    }

    public void displayChanged(GLAutoDrawable glDrawable, boolean b, boolean b1) {
    }

    public void init(GLAutoDrawable glDrawable) {
    }

    public void reshape(GLAutoDrawable glDrawable, int i, int i1, int i2, int i3) {
    }

    public void registerKeyStroke(KeyStroke keyStroke, String description) {
        String modifiersText = KeyEvent.getKeyModifiersText(keyStroke.getModifiers());
        String keyText = KeyEvent.getKeyText(keyStroke.getKeyCode());
        keyboardEntries.add(
                (modifiersText.length() != 0 ? modifiersText + " " : "") +
                keyText + ": " +
                description
        );
    }

    public void registerMouseEvent(int id, int modifiers, String description) {
        String mouseText = null;
        switch (id) {
            case MouseEvent.MOUSE_CLICKED:
                mouseText = "Clicked " + MouseEvent.getModifiersExText(modifiers);
                break;
            case MouseEvent.MOUSE_DRAGGED:
                mouseText = "Dragged " + MouseEvent.getModifiersExText(modifiers);
                break;
            case MouseEvent.MOUSE_ENTERED:
                mouseText = "Mouse enters";
                break;
            case MouseEvent.MOUSE_EXITED:
                mouseText = "Mouse exits";
                break;
            case MouseEvent.MOUSE_MOVED:
                mouseText = "Mouse moves";
                break;
            case MouseEvent.MOUSE_PRESSED:
                mouseText = "Pressed " + MouseEvent.getModifiersExText(modifiers);
                break;
            case MouseEvent.MOUSE_RELEASED:
                mouseText = "Released " + MouseEvent.getModifiersExText(modifiers);
                break;
        }
        mouseEntries.add(
                mouseText + ": " + description
        );

    }
}

class BitmapLoader {
    public static BufferedImage loadBitmap(String file) throws IOException {
        BufferedImage image;
        InputStream input = null;
        try {
            input = ResourceRetriever.getResourceAsStream(file);

            int bitmapFileHeaderLength = 14;
            int bitmapInfoHeaderLength = 40;

            byte bitmapFileHeader[] = new byte[bitmapFileHeaderLength];
            byte bitmapInfoHeader[] = new byte[bitmapInfoHeaderLength];

            input.read(bitmapFileHeader, 0, bitmapFileHeaderLength);
            input.read(bitmapInfoHeader, 0, bitmapInfoHeaderLength);

//            int nSize = bytesToInt(bitmapFileHeader, 2);
            int nWidth = bytesToInt(bitmapInfoHeader, 4);
            int nHeight = bytesToInt(bitmapInfoHeader, 8);
//            int nBiSize = bytesToInt(bitmapInfoHeader, 0);
//            int nPlanes = bytesToShort(bitmapInfoHeader, 12);
            int nBitCount = bytesToShort(bitmapInfoHeader, 14);
            int nSizeImage = bytesToInt(bitmapInfoHeader, 20);
//            int nCompression = bytesToInt(bitmapInfoHeader, 16);
            int nColoursUsed = bytesToInt(bitmapInfoHeader, 32);
//            int nXPixelsMeter = bytesToInt(bitmapInfoHeader, 24);
//            int nYPixelsMeter = bytesToInt(bitmapInfoHeader, 28);
//            int nImportantColours = bytesToInt(bitmapInfoHeader, 36);

            if (nBitCount == 24) {
                image = read24BitBitmap(nSizeImage, nHeight, nWidth, input);
            } else if (nBitCount == 8) {
                image = read8BitBitmap(nColoursUsed, nBitCount, nSizeImage, nWidth, nHeight, input);
            } else {
                System.out.println("Not a 24-bit or 8-bit Windows Bitmap, aborting...");
                image = null;
            }
        } finally {
            try {
                if (input != null)
                    input.close();
            } catch (IOException e) {
            }
        }
        return image;
    }

    private static BufferedImage read8BitBitmap(int nColoursUsed, int nBitCount, int nSizeImage, int nWidth, int nHeight, InputStream input) throws IOException {
        int nNumColors = (nColoursUsed > 0) ? nColoursUsed : (1 & 0xff) << nBitCount;

        if (nSizeImage == 0) {
            nSizeImage = ((((nWidth * nBitCount) + 31) & ~31) >> 3);
            nSizeImage *= nHeight;
        }

        int npalette[] = new int[nNumColors];
        byte bpalette[] = new byte[nNumColors * 4];
        readBuffer(input, bpalette);
        int nindex8 = 0;

        for (int n = 0; n < nNumColors; n++) {
            npalette[n] = (255 & 0xff) << 24 |
                    (bpalette[nindex8 + 2] & 0xff) << 16 |
                    (bpalette[nindex8 + 1] & 0xff) << 8 |
                    (bpalette[nindex8 + 0] & 0xff);

            nindex8 += 4;
        }

        int npad8 = (nSizeImage / nHeight) - nWidth;
        BufferedImage bufferedImage = new BufferedImage(nWidth, nHeight, BufferedImage.TYPE_INT_ARGB);
        DataBufferInt dataBufferByte = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer());
        int[][] bankData = dataBufferByte.getBankData();
        byte bdata[] = new byte[(nWidth + npad8) * nHeight];

        readBuffer(input, bdata);
        nindex8 = 0;

        for (int j8 = nHeight - 1; j8 >= 0; j8--) {
            for (int i8 = 0; i8 < nWidth; i8++) {
                bankData[0][j8 * nWidth + i8] = npalette[((int) bdata[nindex8] & 0xff)];
                nindex8++;
            }
            nindex8 += npad8;
        }

        return bufferedImage;
    }

    private static BufferedImage read24BitBitmap(int nSizeImage, int nHeight, int nWidth, InputStream input) throws IOException {
        int npad = (nSizeImage / nHeight) - nWidth * 3;
        if (npad == 4 || npad < 0)
            npad = 0;
        int nindex = 0;
        BufferedImage bufferedImage = new BufferedImage(nWidth, nHeight, BufferedImage.TYPE_4BYTE_ABGR);
        DataBufferByte dataBufferByte = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer());
        byte[][] bankData = dataBufferByte.getBankData();
        byte brgb[] = new byte[(nWidth + npad) * 3 * nHeight];

        readBuffer(input, brgb);

        for (int j = nHeight - 1; j >= 0; j--) {
            for (int i = 0; i < nWidth; i++) {
                int base = (j * nWidth + i) * 4;
                bankData[0][base] = (byte) 255;
                bankData[0][base + 1] = brgb[nindex];
                bankData[0][base + 2] = brgb[nindex + 1];
                bankData[0][base + 3] = brgb[nindex + 2];
                nindex += 3;
            }
            nindex += npad;
        }

        return bufferedImage;
    }

    private static int bytesToInt(byte[] bytes, int index) {
        return (bytes[index + 3] & 0xff) << 24 |
                (bytes[index + 2] & 0xff) << 16 |
                (bytes[index + 1] & 0xff) << 8 |
                bytes[index + 0] & 0xff;
    }

    private static short bytesToShort(byte[] bytes, int index) {
        return (short) (((bytes[index + 1] & 0xff) << 8) |
                (bytes[index + 0] & 0xff));
    }

    private static void readBuffer(InputStream in, byte[] buffer) throws IOException {
        int bytesRead = 0;
        int bytesToRead = buffer.length;
        while (bytesToRead > 0) {
            int read = in.read(buffer, bytesRead, bytesToRead);
            bytesRead += read;
            bytesToRead -= read;
        }
    }
}


class ResourceRetriever {
    public static URL getResource(final String filename) throws IOException {
        // Try to load resource from jar
        URL url = ResourceRetriever.class.getClassLoader().getResource(filename);
        // If not found in jar, then load from disk
        if (url == null) {
            return new URL("file", "localhost", filename);
        } else {
            return url;
        }
    }

    public static InputStream getResourceAsStream(final String filename) throws IOException {
        // Try to load resource from jar
        String convertedFileName = filename.replace('\\', '/');
        InputStream stream = ResourceRetriever.class.getClassLoader().getResourceAsStream(convertedFileName);
        // If not found in jar, then load from disk
        if (stream == null) {
            return new FileInputStream(convertedFileName);
        } else {
            return stream;
        }
    }
}
